// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (C) 2016-2019, LomboTech Co.Ltd.
 * Author: lomboswer <lomboswer@lombotech.com>
 *
 * memctrl driver - Driver of LomboTech Memctrl Controller
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 */

#ifndef CONFIG_ARCH_LOMBO_N7V1

/* redefine mode name, for PTR_XXX */
#undef MOD_NAME
#define MOD_NAME	"MEMCTRl:monitor"

#include <linux/miscdevice.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/clk.h>
#include <linux/clk-private.h>
#include <linux/err.h>
#include <linux/slab.h>
#include <linux/io.h>
#include <linux/delay.h>
#include <linux/platform_device.h>
#include <linux/of_device.h>
#include <linux/of.h>
#include <linux/pm_runtime.h>
#include <linux/pm_clock.h>
#include <mach/debug.h>
#include <mach/common.h>
#include "memctrl.h"
#include "memctrl_dev.h"

#define reversebit(x, y)	(x ^= (1<<y))
#define getbit(x, y)		((x)>>(y)&1)
#define MASTER_DATA_PER_LINE	2

#undef PRT_ERR
#define PRT_ERR(fmt, ...)       \
	printk(KERN_EMERG "[%s] %s:%d err: "fmt, MOD_NAME, __func__, __LINE__, ##__VA_ARGS__)

static const char separate[3] = {' ', ',', '-'};

/* get monitor from kobject */
static mnt_t kobj_get_mnt(struct kobject *object)
{
	struct device *dev;
	struct lombo_memctrl *mctrl;
	char name[10];
	int index;

	dev = kobj_to_dev(object->parent);
	mctrl = dev_get_drvdata(dev);

	for (index = 0; index < mctrl->ch_number; index++) {
		snprintf(name, sizeof(name)/sizeof(char), "monitor%d", index);
		if (!strcmp(name, object->name))
			break;
		memset(name, 0, sizeof(name)/sizeof(char));
	}

	return mctrl->mnt[index];
}

static ssize_t global_enable_show(struct kobject *kobj,
				struct kobj_attribute *attr, char *buf)
{
	mnt_t mnt;
	ssize_t ret;

	mnt = kobj_get_mnt(kobj);
	ret = sprintf(buf, "%d\n", mnt->global.enable);

	return ret;
}

static ssize_t global_enable_store(struct kobject *kobj,
		struct kobj_attribute *attr, const char *buf, size_t count)
{
	mnt_t mnt;

	mnt = kobj_get_mnt(kobj);
	if (!strncmp(buf, "on", 2) || !strncmp(buf, "1", 1)) {
		mnt->global.enable = 1;
		csp_monitor_set_hif_vlt_int(mnt->base, 1);
		csp_monitor_set_hif_vlt_en(mnt->base, 1);
	} else {
		mnt->global.enable = 0;
		csp_monitor_set_hif_vlt_int(mnt->base, 0);
		csp_monitor_set_hif_vlt_en(mnt->base, 0);
	}

	return count;
}
static struct kobj_attribute global_enable_attr = __ATTR_RW(global_enable);

static ssize_t global_block_show(struct kobject *kobj,
				struct kobj_attribute *attr, char *buf)
{
	mnt_t mnt;
	ssize_t ret;
	unsigned int block;

	mnt = kobj_get_mnt(kobj);
	block = csp_monitor_get_hif_vlt_handle(mnt->base);
	ret = sprintf(buf, "%u\n", block);

	return ret;
}

static ssize_t global_block_store(struct kobject *kobj,
		struct kobj_attribute *attr, const char *buf, size_t count)
{
	mnt_t mnt;

	mnt = kobj_get_mnt(kobj);
	if (!strncmp(buf, "on", 2) || !strncmp(buf, "1", 1))
		csp_monitor_set_hif_vlt_handle(mnt->base, 1);
	else
		csp_monitor_set_hif_vlt_handle(mnt->base, 0);

	return count;
}
static struct kobj_attribute global_block_attr = __ATTR_RW(global_block);

static ssize_t scaling_master_show(struct kobject *kobj,
				struct kobj_attribute *attr, char *buf)
{
	int i;
	ssize_t offset = 0;
	mnt_t mnt;
	struct monitor_master *ms;

	mnt = kobj_get_mnt(kobj);

	for (i = 0; i < mnt->ports; i++) {
		offset += snprintf(buf + offset, PAGE_SIZE - offset,
							"port%d(", i);
		list_for_each_entry(ms, &mnt->master, node)
			if (ms->xpi_id == i)
				offset += snprintf(buf + offset,
							PAGE_SIZE - offset,
							"%s ", ms->name);
		offset += snprintf(buf + offset,
						PAGE_SIZE - offset, ")\n");
	}

	offset += snprintf(buf + offset, PAGE_SIZE - offset, "\n");

	return offset;
}
static struct kobj_attribute scaling_master_attr = __ATTR_RO(scaling_master);

static ssize_t space_enable_show(struct kobject *kobj,
				struct kobj_attribute *attr, char *buf)
{
	mnt_t mnt;
	ssize_t offset = 0;
	unsigned int val, i;
	struct monitor_spacen *payload;

	mnt = kobj_get_mnt(kobj);
	offset += snprintf(buf + offset, PAGE_SIZE - offset,
			"status:%d\n", mnt->space.enable);
	list_for_each_entry(payload, &mnt->space.entry, node) {
		offset += snprintf(buf + offset, PAGE_SIZE - offset,
					"%s(", payload->name);
		val = csp_monitor_get_nw(payload->index, mnt->base);
		for (i = 0; i < mnt->ports; i++) {
			if (getbit(val, i))
				offset += snprintf(buf + offset,
						PAGE_SIZE - offset,
						"port%d ", i);
		}
		offset += snprintf(buf + offset, PAGE_SIZE - offset, "):");
		offset += snprintf(buf + offset, PAGE_SIZE - offset,
			"0x%x-0x%x\n", payload->s_addr, payload->e_addr);
	}

	return offset;
}

static ssize_t space_enable_store(struct kobject *kobj,
		struct kobj_attribute *attr, const char *buf, size_t count)
{
	mnt_t mnt;

	mnt = kobj_get_mnt(kobj);
	if (!strncmp(buf, "on", 2) || !strncmp(buf, "1", 1)) {
		mnt->space.enable = 1;
		csp_monitor_set_space_mnt_int(mnt->base, 1);
		csp_monitor_set_space_mnt_en(mnt->base, 1);
		/* also open global monitor */
		mnt->global.enable = 1;
		csp_monitor_set_hif_vlt_int(mnt->base, 1);
		csp_monitor_set_hif_vlt_en(mnt->base, 1);
	} else {
		mnt->space.enable = 0;
		csp_monitor_set_space_mnt_int(mnt->base, 0);
		csp_monitor_set_space_mnt_en(mnt->base, 0);
		mnt->global.enable = 0;
		csp_monitor_set_hif_vlt_int(mnt->base, 0);
		csp_monitor_set_hif_vlt_en(mnt->base, 0);
	}

	return count;
}
static struct kobj_attribute space_enable_attr = __ATTR_RW(space_enable);

static struct attribute *monitor_attrs[] = {
	&global_enable_attr.attr,
	&global_block_attr.attr,
	&scaling_master_attr.attr,
	&space_enable_attr.attr,
	/* sentry */
	NULL,
};

static struct attribute_group monitor_attr_group = {
	.attrs = monitor_attrs,
};

static ssize_t space_addr_show(struct kobject *kobj,
				struct kobj_attribute *attr, char *buf)
{
	mnt_t mnt;
	ssize_t ret = 0;
	int index = -1;
	struct monitor_spacen *payload;

	mnt = kobj_get_mnt(kobj->parent);
	list_for_each_entry(payload, &mnt->space.entry, node) {
		if (!strcmp(kobject_name(kobj), payload->name)) {
			index = payload->index;
			break;
		}
	}
	/* if not patch any master */
	if (index == -1)
		goto exit;

	ret = sprintf(buf, "upper:0x%x bottom:0x%x\n",
				(u32)payload->s_addr, (u32)payload->e_addr);

exit:
	return ret;
}

static ssize_t space_addr_store(struct kobject *kobj,
		struct kobj_attribute *attr, const char *buf, size_t count)
{
	mnt_t mnt;
	int index = -1, i, ret;
	struct monitor_spacen *payload;
	char *addr_bt;
	unsigned int s_addr, e_addr;

	mnt = kobj_get_mnt(kobj->parent);
	list_for_each_entry(payload, &mnt->space.entry, node) {
		if (!strcmp(kobject_name(kobj), payload->name)) {
			index = payload->index;
			break;
		}
	}
	/* if not patch any master */
	if (index == -1)
		goto exit;

	for (i = 0; i < sizeof(separate); i++)
		if (strpbrk(buf, &separate[i]))
			break;
	/**
	 * if not found separate character in separate[],
	 * there may has wrong input, just exit.
	 */
	if (i >= sizeof(separate))
		goto exit;

	addr_bt = strpbrk(buf, &separate[i]) + 1;
	ret = sscanf(buf, "%x", &s_addr);
	ret = sscanf(addr_bt, "%x", &e_addr);
	PRT_DBG("0x%x 0x%x\n", s_addr, e_addr);
	if (s_addr > e_addr) {
		PRT_ERR("start addr[0x%x] is behind end addr[0x%x]\n",
				s_addr, e_addr);
		goto exit;
	}
	payload->s_addr = s_addr;
	payload->e_addr = e_addr;
	csp_monitor_set_high24_sa(payload->index, mnt->base, payload->s_addr);
	csp_monitor_set_high24_ea(payload->index, mnt->base, payload->e_addr);

exit:
	return count;
}
static struct kobj_attribute space_addr_attr = __ATTR_RW(space_addr);

static ssize_t nw_ip_show(struct kobject *kobj,
				struct kobj_attribute *attr, char *buf)
{
	mnt_t mnt;
	ssize_t ret = 0;
	unsigned int val;
	int index = -1;
	struct monitor_spacen *payload;

	mnt = kobj_get_mnt(kobj->parent);
	list_for_each_entry(payload, &mnt->space.entry, node) {
		if (!strcmp(kobject_name(kobj), payload->name)) {
			index = payload->index;
			break;
		}
	}
	/* if not patch any master */
	if (index == -1)
		goto exit;

	val = csp_monitor_get_nw(index, mnt->base);
	ret = sprintf(buf, "0x%x\n", val);

exit:
	return ret;
}

static ssize_t nw_ip_store(struct kobject *kobj,
		struct kobj_attribute *attr, const char *buf, size_t count)
{
	mnt_t mnt;
	int i, index = -1;
	unsigned int val;
	struct monitor_spacen *payload;

	mnt = kobj_get_mnt(kobj->parent);
	list_for_each_entry(payload, &mnt->space.entry, node) {
		if (!strcmp(kobject_name(kobj), payload->name)) {
			index = payload->index;
			break;
		}
	}
	/* if not patch any master */
	if (index == -1)
		goto exit;

	if (kstrtou32(buf, 10, &i))
		goto exit;
	if (i < 0 || i > mnt->ports) {
		PRT_ERR("port%d err", i);
		goto exit;
	}

	val = csp_monitor_get_nw(index, mnt->base);
	/* if user second write the same port id, just close this port */
	reversebit(val, i);
	csp_monitor_set_nw(index, mnt->base, val);

exit:
	return count;
}
static struct kobj_attribute nw_ip_attr = __ATTR_RW(nw_ip);

static struct attribute *space_attrs[] = {
	&space_addr_attr.attr,
	&nw_ip_attr.attr,
	/* sentry */
	NULL,
};

static struct attribute_group space_attr_group = {
	.attrs = space_attrs,
};

int of_create_space_node(struct device *dev, int channel)
{
	int counter, ret = 0;
	struct monitor_spacen *node;
	struct lombo_memctrl *mctrl;
	mnt_t mnt;

	mctrl = dev_get_drvdata(dev);
	mnt = mctrl->mnt[channel];
	if (!dev->of_node)
		return -EINVAL;

	ret = of_property_read_u32(mnt->of_node, "space_counter", &counter);
	if (ret)
		return -EINVAL;
	PRT_DBG("support %d monitor of memeory\n", counter);
	mnt->space.count = counter;

	while (counter--) {
		node = devm_kmalloc(dev, sizeof(struct monitor_spacen),
							GFP_KERNEL);
		if (!node) {
			PRT_ERR("failed to alloc memory\n");
			return -ENOMEM;
		}
		ret = snprintf(node->name, sizeof(node->name), "space%d",
								counter);
		list_add(&node->node, &mnt->space.entry);
		node->index = counter;
		node->s_addr = 0x0;
		node->e_addr = 0x0;
		/* create space kobject, for create spaceN dir */
		node->kobj = kobject_create_and_add(node->name, mnt->kobj);
		ret = sysfs_create_group(node->kobj, &space_attr_group);
		if (ret) {
			PRT_DBG("%s sysfs attribute group not created\n",
								node->name);
			return -EINVAL;
		}
	}

	return 0;
}

int of_create_master_list(struct device *dev, int channel)
{
	int count, cnt, port, index;
	mnt_t mnt;
	struct lombo_memctrl *mctrl;
	struct monitor_master *ms;
	const char *name;
	struct table {
		unsigned int port;
		unsigned int count;
	};
	struct table *id_table;

	if (!dev->of_node)
		return -EINVAL;

	mctrl = dev_get_drvdata(dev);
	mnt = mctrl->mnt[channel];

	cnt = of_property_count_u32_elems(mnt->of_node, "id-table");
	if (cnt <= 0) {
		PRT_ERR("master_id num %d err\n", cnt);
		return -EINVAL;
	}
	id_table = devm_kmalloc_array(dev, cnt, sizeof(unsigned int),
								GFP_KERNEL);
	if (!id_table) {
		PRT_ERR("failed to alloc memory\n");
		return -ENOMEM;
	}
	if (of_property_read_u32_array(mnt->of_node, "id-table",
						(u32 *)id_table, cnt)) {
		PRT_ERR("read id-table err\n");
		return -ENOMEM;
	}

	cnt /= MASTER_DATA_PER_LINE;
	mnt->ports = cnt;
	index = 0;
	for (port = 0; port < cnt; port++) {
		for (count = 0; count < id_table[port].count; count++) {
			ms = devm_kmalloc(dev, sizeof(struct monitor_master),
								GFP_KERNEL);
			if (!ms) {
				PRT_ERR("failed to alloc memory\n");
				return -ENOMEM;
			}
			of_property_read_string_index(mnt->of_node, "masters",
							index, &name);
			strncpy(ms->name, name, 16);
			ms->xpi_id = port;
			ms->id = count;
			list_add(&ms->node, &mnt->master);
			index++;
			PRT_DBG("%s:%d:%d", ms->name, ms->xpi_id, ms->id);
		}
	}

	devm_kfree(dev, (void *)id_table);
	return 0;
}

int check_global_monitor(mnt_t mnt)
{
	void *base = mnt->base;
	int ret = 0;

	ret = csp_monitor_get_vlt_status(base);
	if (ret) {
	/* update is not zero, means last event is working,don't update info */
		if (mnt->global.update)
			goto clear;

		mnt->global.info.type = csp_monitor_get_hif_vlt_type(base);
		mnt->global.info.addr = csp_monitor_get_vlt_addr(base);
		mnt->global.info.id = csp_monitor_get_vlt_id(base);
		mnt->global.info.xpi_id = csp_monitor_get_vlt_xpi_id(base);
		mnt->global.update = 1;
clear:
		/* clear pending */
		csp_monitor_vlt_stat_clear(base);
	}

	return ret;
}
EXPORT_SYMBOL_GPL(check_global_monitor);

int check_space_monitor(mnt_t mnt)
{
	void *base = mnt->base;
	int ret = 0;

	ret = csp_monitor_get_space_mnt_status(base);
	if (ret) {
		if (mnt->space.update)
			goto clear;

		mnt->space.info.type = csp_monitor_get_space_mnt_type(base);
		mnt->space.info.addr = csp_monitor_get_space_mnt_addr(base);
		mnt->space.info.id = csp_monitor_get_space_mnt_id(base);
	       mnt->space.info.xpi_id = csp_monitor_get_space_mnt_xpi_id(base);
		mnt->space.update = 1;
clear:
		/* clear pending */
		csp_monitor_space_mnt_stat_clear(base);
	}

	return ret;
}
EXPORT_SYMBOL_GPL(check_space_monitor);

static void mnt_print_info(mnt_t mnt, char *name, struct violation_info *info)
{
	int ms_id;
	struct monitor_master *ms;

	static const char *type[3] = {"write", "read",
					"read modify write"};

	PRT_ERR("monitor: %s\n", name);
	PRT_ERR("violation type: %s\n", type[info->type]);
	PRT_ERR("violation address: 0x%x\n", info->addr + 0x40000000);
	PRT_ERR("violation xpi_id: %d\n", info->xpi_id);
	PRT_ERR("violation id: %d\n", info->id);
	/* bit[11-8] means master id */
	ms_id = (info->id >> 8);

	list_for_each_entry(ms, &mnt->master, node) {
		if (ms->xpi_id == info->xpi_id && ms->id == ms_id) {
			PRT_ERR("violation master: %s\n", ms->name);
			break;
		}
	}
}

static void handle_monitor(struct work_struct *work)
{
	int old_cpu, this_cpu;
	mnt_t mnt = container_of(work, struct monitor, update);

	if (mnt->global.update) {
		mnt->global.update = 0;
		mnt_print_info(mnt, "global", &mnt->global.info);
		local_irq_disable();
		this_cpu = raw_smp_processor_id();
		old_cpu  = atomic_cmpxchg(&panic_cpu, PANIC_CPU_INVALID, this_cpu);
		if (old_cpu != PANIC_CPU_INVALID && old_cpu != this_cpu){
			while (1)
			cpu_relax();
		}
		smp_send_stop();
		while(1);
	}
	if (mnt->space.update) {
		mnt->space.update = 0;
		mnt_print_info(mnt, "space", &mnt->space.info);
	}
}

int mnt_setup(struct device *dev)
{
	int channel, ret = 0;
	struct lombo_memctrl *mctrl;
	mnt_t mnt;
	struct device_node *child;
	char name[10];

	/**
	 * if have not monitor in dts, quit at once.
	 * because there has many case not update
	 */
	child = of_get_child_by_name(dev->of_node, "monitor");
	if (!child) {
		PRT_WARN("not found monitor node in memctrl\n");
		return 0;
	}

	mctrl = dev_get_drvdata(dev);

	for (channel = 0; channel < mctrl->ch_number; channel++) {
		mnt = devm_kzalloc(dev, sizeof(struct monitor), GFP_KERNEL);
		if (!mnt) {
			PRT_ERR("failed to alloc monitor memory\n");
			return -ENOMEM;
		}
		mctrl->mnt[channel] = mnt;
		/* save device node handle */
		mnt->of_node = child;

		if (!mctrl->ddr_bitbase[channel])
			return -EINVAL;
		mnt->base = mctrl->ddr_bitbase[channel];

		mnt->parent = dev;
		INIT_LIST_HEAD(&mnt->master);
		/* init global */
		mnt->global.enable = 0;
		mnt->global.update = 0; /* update should be atomic_t */
		/* init space */
		mnt->space.enable = 0;
		mnt->space.update = 0;
		INIT_LIST_HEAD(&mnt->space.entry);

		INIT_WORK(&mnt->update, handle_monitor);

		snprintf(name, sizeof(name)/sizeof(char), "monitor%d", channel);
		/* create monitor kobject, for create monitor dir */
		mnt->kobj = kobject_create_and_add(name,
					&mctrl->miscdev.this_device->kobj);
		ret = sysfs_create_group(mnt->kobj, &monitor_attr_group);
		if (ret)
			PRT_ERR("sysfs attribute group not created\n");

		/* init spaceN */
		ret = of_create_space_node(mnt->parent, channel);
		if (ret)
			return -EINVAL;

		ret = of_create_master_list(mnt->parent, channel);
		if (ret)
			return -EINVAL;
	}

	return ret;
}
EXPORT_SYMBOL_GPL(mnt_setup);

int mnt_destroy(struct device *dev)
{
	mnt_t mnt;
	int channel;
	struct lombo_memctrl *mctrl;
	struct monitor_spacen *payload;

	mctrl = dev_get_drvdata(dev);

	for (channel = 0; channel < mctrl->ch_number; channel++) {
		/* if not register monitor, just return */
		if (!mctrl->mnt[channel])
			return 0;

		mnt = mctrl->mnt[channel];
		/* remove space kobject and group */
		list_for_each_entry(payload, &mnt->space.entry, node) {
			sysfs_remove_group(payload->kobj, &space_attr_group);
			kobject_del(payload->kobj);
		}
		/* remove monitor kobject and group */
		sysfs_remove_group(mnt->kobj, &monitor_attr_group);
		kobject_del(mnt->kobj);
	}
	return 0;
}
EXPORT_SYMBOL_GPL(mnt_destroy);

#endif /* CONFIG_ARCH_LOMBO_N7V1 */
